local M = {}
M.methods = {}
local Log = require"user.log"

local luasnip_installed, _ = pcall(require, "luasnip")
local ultisnips_installed, cmp_ultisnips_mappings = pcall(require, "cmp_nvim_ultisnips.mappings")

local engine = "luasnip"

---checks if the character preceding the cursor is a space character
---@return boolean true if it is a space character, false otherwise
local check_backspace = function()
  local col = vim.fn.col "." - 1
  return col == 0 or vim.fn.getline("."):sub(col, col):match "%s"
end
M.methods.check_backspace = check_backspace

local function T(str)
  return vim.api.nvim_replace_termcodes(str, true, true, true)
end

---wraps vim.fn.feedkeys while replacing key codes with escape codes
---Ex: feedkeys("<CR>", "n") becomes feedkeys("^M", "n")
---@param key string
---@param mode string
local function feedkeys(key, mode)
  vim.fn.feedkeys(T(key), mode)
end
M.methods.feedkeys = feedkeys

---checks if emmet_ls is available and active in the buffer
---@return boolean true if available, false otherwise
local is_emmet_active = function()
  local clients = vim.lsp.buf_get_clients()

  for _, client in pairs(clients) do
    if client.name == "emmet_ls" then
      return true
    end
  end
  return false
end
M.methods.is_emmet_active = is_emmet_active

---when inside a snippet, seeks to the nearest luasnip field if possible, and checks if it is jumpable
---@param dir? number 1 for forward, -1 for backward; defaults to 1
---@return boolean true if a jumpable luasnip field is found while inside a snippet
local function jumpable(dir)
  local luasnip_ok, luasnip = pcall(require, "luasnip")
  if not luasnip_ok then
    return
  end

  local win_get_cursor = vim.api.nvim_win_get_cursor
  local get_current_buf = vim.api.nvim_get_current_buf

  local function inside_snippet()
    -- for outdated versions of luasnip
    if not luasnip.session.current_nodes then
      return false
    end

    local node = luasnip.session.current_nodes[get_current_buf()]
    if not node then
      return false
    end

    local snip_begin_pos, snip_end_pos = node.parent.snippet.mark:pos_begin_end()
    local pos = win_get_cursor(0)
    pos[1] = pos[1] - 1 -- LuaSnip is 0-based not 1-based like nvim for rows
    return pos[1] >= snip_begin_pos[1] and pos[1] <= snip_end_pos[1]
  end

  ---sets the current buffer's luasnip to the one nearest the cursor
  ---@return boolean true if a node is found, false otherwise
  local function seek_luasnip_cursor_node()
    -- for outdated versions of luasnip
    if not luasnip.session.current_nodes then
      return false
    end

    local pos = win_get_cursor(0)
    pos[1] = pos[1] - 1
    local node = luasnip.session.current_nodes[get_current_buf()]
    if not node then
      return false
    end

    local snippet = node.parent.snippet
    local exit_node = snippet.insert_nodes[0]

    -- exit early if we're past the exit node
    if exit_node then
      local exit_pos_end = exit_node.mark:pos_end()
      if (pos[1] > exit_pos_end[1]) or (pos[1] == exit_pos_end[1] and pos[2] > exit_pos_end[2]) then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end
    end

    node = snippet.inner_first:jump_into(1, true)
    while node ~= nil and node.next ~= nil and node ~= snippet do
      local n_next = node.next
      local next_pos = n_next and n_next.mark:pos_begin()
      local candidate = n_next ~= snippet and next_pos and (pos[1] < next_pos[1])
        or (pos[1] == next_pos[1] and pos[2] < next_pos[2])

      -- Past unmarked exit node, exit early
      if n_next == nil or n_next == snippet.next then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end

      if candidate then
        luasnip.session.current_nodes[get_current_buf()] = node
        return true
      end

      local ok
      ok, node = pcall(node.jump_from, node, 1, true) -- no_move until last stop
      if not ok then
        snippet:remove_from_jumplist()
        luasnip.session.current_nodes[get_current_buf()] = nil

        return false
      end
    end

    -- No candidate, but have an exit node
    if exit_node then
      -- to jump to the exit node, seek to snippet
      luasnip.session.current_nodes[get_current_buf()] = snippet
      return true
    end

    -- No exit node, exit from snippet
    snippet:remove_from_jumplist()
    luasnip.session.current_nodes[get_current_buf()] = nil
    return false
  end

  if dir == -1 then
    return inside_snippet() and luasnip.jumpable(-1)
  else
    return inside_snippet() and seek_luasnip_cursor_node() and luasnip.jumpable()
  end
end
M.methods.jumpable = jumpable

M.config = function()
  local status_cmp_ok, cmp = pcall(require, "cmp")
  if not status_cmp_ok then
    return
  end

  local status_luasnip_ok, luasnip = pcall(require, "luasnip")
  if not status_luasnip_ok and engine == "luasnip" then
    return
  end

  userconf.builtin.cmp = {
    confirm_opts = {
      behavior = cmp.ConfirmBehavior.Replace,
      select = false,
    },
    completion = {
      ---@usage The minimum length of a word to complete on.
      keyword_length = 1,
    },
    experimental = {
      ghost_text = true,
      native_menu = false,
    },
    formatting = {
      fields = { "kind", "abbr", "menu" },
      max_width = 0,
      kind_icons = {
        Class = " ",
        Color = " ",
        Constant = "ﲀ ",
        Constructor = " ",
        Enum = "練",
        EnumMember = " ",
        Event = " ",
        Field = " ",
        File = "",
        Folder = " ",
        Function = " ",
        Interface = "ﰮ ",
        Keyword = " ",
        Method = " ",
        Module = " ",
        Operator = "",
        Property = " ",
        Reference = " ",
        Snippet = " ",
        Struct = " ",
        Text = " ",
        TypeParameter = " ",
        Unit = "塞",
        Value = " ",
        Variable = " ",
      },
      source_names = {
        nvim_lsp = "(LSP)",
        emoji = "(Emoji)",
        path = "(Path)",
        calc = "(Calc)",
        cmp_tabnine = "(Tabnine)",
        vsnip = "(Snippet)",
        luasnip = "(Snippet)",
        ultisnips = "(Snippet)",
        buffer = "(Buffer)",
        tmux = "(TMUX)",
        -- nvim_lua = "(NVim)"
      },
      duplicates = {
        buffer = 1,
        path = 1,
        nvim_lsp = 0,
        luasnip = 1,
      },
      duplicates_default = 0,
      format = function(entry, vim_item)
        local max_width = userconf.builtin.cmp.formatting.max_width
        if max_width ~= 0 and #vim_item.abbr > max_width then
          vim_item.abbr = string.sub(vim_item.abbr, 1, max_width - 1) .. "…"
        end
        if userconf.use_icons then
          vim_item.kind = userconf.builtin.cmp.formatting.kind_icons[vim_item.kind]
        end
        vim_item.menu = userconf.builtin.cmp.formatting.source_names[entry.source.name]
        vim_item.dup = userconf.builtin.cmp.formatting.duplicates[entry.source.name]
          or userconf.builtin.cmp.formatting.duplicates_default
        return vim_item
      end,
    },
    snippet = {
      expand = function(args)
        if engine == "ultisnips" then
          vim.fn["UltiSnips#Anon"](args.body)
        elseif engine == "luasnip" then
          require("luasnip").lsp_expand(args.body)
        end
      end,
    },
    window = {
      completion = cmp.config.window.bordered(),
      documentation = cmp.config.window.bordered(),
    },
    sources = {
      { name = "ultisnips" },
      { name = "luasnip" },
      { name = "nvim_lsp" },
      { name = "path" },
      { name = "cmp_tabnine" },
      { name = "nvim_lua" },
      { name = "buffer" },
      { name = "calc" },
      { name = "emoji" },
      { name = "treesitter" },
      { name = "crates" },
      { name = "tmux" },
      -- { name = 'nvim_lua' },
    },
    mapping = cmp.mapping.preset.insert {
      ["<C-k>"] = cmp.mapping.select_prev_item(),
      ["<C-j>"] = cmp.mapping.select_next_item(),
      ["<C-d>"] = cmp.mapping.scroll_docs(-4),
      ["<C-f>"] = cmp.mapping.scroll_docs(4),
      -- TODO: potentially fix emmet nonsense
      ["<Tab>"] = cmp.mapping(function(fallback)
        if engine == "ultisnips" then
          cmp_ultisnips_mappings.expand_or_jump_forwards(fallback)
        elseif engine == "luasnip" then
          if cmp.visible() then
            cmp.select_next_item()
          elseif luasnip.expandable() then
            luasnip.expand()
          elseif jumpable() then
            luasnip.jump(1)
          elseif check_backspace() then
            fallback()
          elseif is_emmet_active() then
            return vim.fn["cmp#complete"]()
          else
            fallback()
          end
        end
      end, {
        "i",
        "s",
      }),
      ["<S-Tab>"] = cmp.mapping(function(fallback)
        if engine == "ultisnips" then
          cmp_ultisnips_mappings.jump_backwards(fallback)
        elseif engine == "luasnip" then
          if cmp.visible() then
            cmp.select_prev_item()
          elseif jumpable(-1) then
            luasnip.jump(-1)
          else
            fallback()
          end
        end
      end, {
        "i",
        "s",
      }),

      ["<C-Space>"] = cmp.mapping.complete(),
      -- ["<C-e>"] = cmp.mapping.abort(),

      --[[ ["<CR>"] = cmp.mapping(function(fallback) ]]
      --[[   if engine == "ultisnips" then ]]
      --[[     if cmp.visible() and cmp.confirm(userconf.builtin.cmp.confirm_opts) then ]]
      --[[       return ]]
      --[[     end ]]
      --[[]]
      --[[     fallback() ]]
      --[[   elseif engine == "luasnip" then ]]
      --[[     if cmp.visible() and cmp.confirm(userconf.builtin.cmp.confirm_opts) then ]]
      --[[       if jumpable() then ]]
      --[[         luasnip.jump(1) ]]
      --[[       end ]]
      --[[       return ]]
      --[[     end ]]
      --[[]]
      --[[     if jumpable() then ]]
      --[[       if not luasnip.jump(1) then ]]
      --[[         fallback() ]]
      --[[       end ]]
      --[[     else ]]
      --[[       fallback() ]]
      --[[     end ]]
      --[[   end ]]
      --[[ end), ]]
      ["<CR>"] = cmp.mapping.confirm({ select = true }),

    },
  }
end

function M.setup()
  local cmp_ok, cmp = pcall(require, "cmp")
  if not cmp_ok then
    Log:error("cmp not installed!")
    return
  end

  cmp.setup(userconf.builtin.cmp)

  local cmp_nvim_ultisnips_installed, cmp_nvim_ultisnips = pcall(require, "cmp_nvim_ultisnips")
  if cmp_nvim_ultisnips_installed then
    cmp_nvim_ultisnips.setup{}
  end
end

return M
